// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using ILLink.Shared.TypeSystemProxy;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Collections.Generic;

namespace Mono.Linker.Steps
{
    //
    // Evaluates simple properties or methods for constant expressions and
    // then uses this information to remove unreachable conditional blocks and
    // inline collected constants.
    //
    public class UnreachableBlocksOptimizer
    {
        readonly LinkContext _context;
        readonly Dictionary<MethodDefinition, MethodResult?> _cache_method_results = new(2048);
        readonly Stack<MethodDefinition> _resursion_guard = new();

        MethodDefinition? IntPtrSize, UIntPtrSize;

        public UnreachableBlocksOptimizer(LinkContext context)
        {
            _context = context;
        }

        /// <summary>
        /// Processes the specified method and perform all branch removal optimizations on it.
        /// When this returns it's guaranteed that the method has been optimized (if possible).
        /// </summary>
        /// <param name="method">The method to process</param>
        public void ProcessMethod(MethodDefinition method)
        {
            if (!IsMethodSupported(method))
                return;

            if (_context.Annotations.GetAction(method.Module.Assembly) != AssemblyAction.Link)
                return;

            var reducer = new BodyReducer(method.Body, _context);

            try
            {
                //
                // If no external dependency can be extracted into constant there won't be
                // anything to optimize in the method
                //
                if (!reducer.ApplyTemporaryInlining(this))
                    return;

                //
                // This is the main step which evaluates if any expression can
                // produce folded branches. When it finds them the unreachable
                // branch is removed.
                //
                if (reducer.RewriteBody())
                    _context.LogMessage($"Reduced '{reducer.InstructionsReplaced}' instructions in conditional branches for [{method.DeclaringType.Module.Assembly.Name}] method '{method.GetDisplayName()}'.");

                //
                // Note: The inliner cannot run before reducer rewrites body as it
                // would require another recomputing offsets due to instructions replacement
                // done by inliner
                //
                var inliner = new CallInliner(method.Body, this);
                inliner.RewriteBody();
            }
            catch (Exception e)
            {
                throw new InternalErrorException($"Could not process the body of method '{method.GetDisplayName()}'.", e);
            }
        }

        static bool IsMethodSupported(MethodDefinition method)
        {
            if (!method.HasBody)
                return false;

            //
            // Block methods which rewrite does not support
            //
            switch (method.ReturnType.MetadataType)
            {
                case MetadataType.ByReference:
                case MetadataType.FunctionPointer:
                    return false;
            }

            return true;
        }

        static bool HasJumpIntoTargetRange(Collection<Instruction> instructions, int firstInstr, int lastInstr, Func<Instruction, int?>? mapping = null)
        {
            foreach (var instr in instructions)
            {
                switch (instr.OpCode.FlowControl)
                {
                    case FlowControl.Branch:
                    case FlowControl.Cond_Branch:
                        if (instr.Operand is Instruction target)
                        {
                            if (mapping != null && mapping(target) is int index)
                            {
                                if (index >= firstInstr && index <= lastInstr)
                                {
                                    return true;
                                }
                            }
                            else
                            {
                                for (int i = firstInstr; i <= lastInstr; i++)
                                {
                                    if (instructions[i] == target)
                                    {
                                        return true;
                                    }
                                }
                            }
                        }
                        else
                        {
                            foreach (var rtarget in (Instruction[])instr.Operand)
                            {
                                if (mapping != null && mapping(rtarget) is int index)
                                {
                                    if (index >= firstInstr && index <= lastInstr)
                                    {
                                        return true;
                                    }
                                }
                                else
                                {
                                    for (int i = firstInstr; i <= lastInstr; i++)
                                    {
                                        if (instructions[i] == rtarget)
                                        {
                                            return true;
                                        }
                                    }
                                }
                            }
                        }

                        break;
                }
            }

            return false;
        }

        static bool IsSideEffectFreeLoad(Instruction instr)
        {
            switch (instr.OpCode.Code)
            {
                case Code.Ldarg:
                case Code.Ldloc:
                case Code.Ldloc_0:
                case Code.Ldloc_1:
                case Code.Ldloc_2:
                case Code.Ldloc_3:
                case Code.Ldloc_S:
                case Code.Ldc_I4_0:
                case Code.Ldc_I4_1:
                case Code.Ldc_I4_2:
                case Code.Ldc_I4_3:
                case Code.Ldc_I4_4:
                case Code.Ldc_I4_5:
                case Code.Ldc_I4_6:
                case Code.Ldc_I4_7:
                case Code.Ldc_I4_8:
                case Code.Ldc_I4:
                case Code.Ldc_I4_S:
                case Code.Ldc_I4_M1:
                case Code.Ldc_I8:
                case Code.Ldc_R4:
                case Code.Ldc_R8:
                case Code.Ldnull:
                case Code.Ldstr:
                    return true;
            }

            return false;
        }

        static bool IsComparisonAlwaysTrue(OpCode opCode, int left, int right)
        {
            switch (opCode.Code)
            {
                case Code.Beq:
                case Code.Beq_S:
                case Code.Ceq:
                    return left == right;
                case Code.Bne_Un:
                case Code.Bne_Un_S:
                    return left != right;
                case Code.Bge:
                case Code.Bge_S:
                    return left >= right;
                case Code.Bge_Un:
                case Code.Bge_Un_S:
                    return (uint)left >= (uint)right;
                case Code.Bgt:
                case Code.Bgt_S:
                case Code.Cgt:
                    return left > right;
                case Code.Bgt_Un:
                case Code.Bgt_Un_S:
                    return (uint)left > (uint)right;
                case Code.Ble:
                case Code.Ble_S:
                    return left <= right;
                case Code.Ble_Un:
                case Code.Ble_Un_S:
                    return (uint)left <= (uint)right;
                case Code.Blt:
                case Code.Blt_S:
                case Code.Clt:
                    return left < right;
                case Code.Blt_Un:
                case Code.Blt_Un_S:
                    return (uint)left < (uint)right;
            }

            throw new NotImplementedException(opCode.ToString());
        }

        MethodResult? AnalyzeMethodForConstantResult(in CalleePayload callee, Stack<MethodDefinition> callStack)
        {
            MethodDefinition method = callee.Method;

            if (method.ReturnType.MetadataType == MetadataType.Void)
                return null;

            if (!method.HasBody)
                return null;

            switch (_context.Annotations.GetAction(method))
            {
                case MethodAction.ConvertToThrow:
                    return null;
                case MethodAction.ConvertToStub:
                    Instruction? constant = CodeRewriterStep.CreateConstantResultInstruction(_context, method);
                    return constant == null ? null : new MethodResult(constant, !HasSideEffects(method));
            }

            if (method.IsIntrinsic() || method.NoInlining)
                return null;

            if (!_context.IsOptimizationEnabled(CodeOptimizations.IPConstantPropagation, method))
                return null;

            var analyzer = new ConstantExpressionMethodAnalyzer(this);
            if (analyzer.Analyze(callee, callStack))
                return new MethodResult(analyzer.Result, analyzer.SideEffectFreeResult);

            return null;
        }

        static bool HasSideEffects(MethodDefinition method)
        {
            return !method.DeclaringType.IsBeforeFieldInit;
        }

        //
        // Return expression with a value when method implementation can be
        // interpreted during trimming
        //
        MethodResult? TryGetMethodCallResult(in CalleePayload callee)
        {
            _resursion_guard.Clear();
            return TryGetMethodCallResult(callee, _resursion_guard);
        }

        MethodResult? TryGetMethodCallResult(in CalleePayload callee, Stack<MethodDefinition> callStack)
        {
            MethodResult? value;

            MethodDefinition method = callee.Method;
            if (!method.HasMetadataParameters() || callee.HasUnknownArguments)
            {
                if (!_cache_method_results.TryGetValue(method, out value) && !IsDeepStack(callStack))
                {
                    value = AnalyzeMethodForConstantResult(callee, callStack);
                    _cache_method_results.Add(method, value);
                }

                return value;
            }

            return AnalyzeMethodForConstantResult(callee, callStack);

            static bool IsDeepStack(Stack<MethodDefinition> callStack) => callStack.Count > 100;
        }

        Instruction? GetSizeOfResult(TypeReference type)
        {
            MethodDefinition? sizeOfImpl = null;

            //
            // sizeof (IntPtr) and sizeof (UIntPtr) are just aliases for IntPtr.Size and UIntPtr.Size
            // which are simple static properties commonly overwritten. Instead of forcing C# code style
            // we handle both via static get_Size method
            //
            if (type.MetadataType == MetadataType.UIntPtr)
            {
                sizeOfImpl = (UIntPtrSize ??= FindSizeMethod(_context.TryResolve(type)));
            }
            else if (type.MetadataType == MetadataType.IntPtr)
            {
                sizeOfImpl = (IntPtrSize ??= FindSizeMethod(_context.TryResolve(type)));
            }

            if (sizeOfImpl == null)
                return null;

            return TryGetMethodCallResult(new CalleePayload(sizeOfImpl, Array.Empty<Instruction>()))?.Instruction;
        }

        static Instruction? EvaluateIntrinsicCall(MethodReference method, Instruction[] arguments)
        {
            //
            // In theory any pure method could be executed via reflection but
            // that would require loading all code path dependencies.
            // For now we handle only few methods that help with core framework trimming
            //
            object? left, right;
            if (method.DeclaringType.MetadataType == MetadataType.String)
            {
                switch (method.Name)
                {
                    case "op_Equality":
                    case "op_Inequality":
                    case "Concat":
                        if (arguments.Length != 2)
                            return null;

                        if (!GetConstantValue(arguments[0], out left) ||
                            !GetConstantValue(arguments[1], out right))
                            return null;

                        if (left is string sleft && right is string sright)
                        {
                            if (method.Name.Length == 6) // Concat case
                                return Instruction.Create(OpCodes.Ldstr, string.Concat(sleft, sright));

                            bool result = method.Name.Length == 11 ? sleft == sright : sleft != sright;
                            return Instruction.Create(OpCodes.Ldc_I4, result ? 1 : 0); // op_Equality / op_Inequality
                        }

                        break;
                }
            }

            return null;
        }

        static Instruction[]? GetArgumentsOnStack(MethodDefinition method, Collection<Instruction> instructions, int index)
        {
            if (!method.HasMetadataParameters())
                return Array.Empty<Instruction>();

            Instruction[]? result = null;
            for (int i = method.GetMetadataParametersCount(), pos = 0; i != 0; --i, ++pos)
            {
                Instruction instr = instructions[index - i];
                if (!IsConstantValue(instr))
                    return null;

                result ??= new Instruction[method.GetMetadataParametersCount()];

                result[pos] = instr;
            }

            if (result != null && HasJumpIntoTargetRange(instructions, index - method.GetMetadataParametersCount() + 1, index))
                return null;

            return result;

            static bool IsConstantValue(Instruction instr)
            {
                switch (instr.OpCode.Code)
                {
                    case Code.Ldc_I4_0:
                    case Code.Ldc_I4_1:
                    case Code.Ldc_I4_2:
                    case Code.Ldc_I4_3:
                    case Code.Ldc_I4_4:
                    case Code.Ldc_I4_5:
                    case Code.Ldc_I4_6:
                    case Code.Ldc_I4_7:
                    case Code.Ldc_I4_8:
                    case Code.Ldc_I4:
                    case Code.Ldc_I4_S:
                    case Code.Ldc_I4_M1:
                    case Code.Ldc_I8:
                    case Code.Ldc_R4:
                    case Code.Ldc_R8:
                    case Code.Ldnull:
                    case Code.Ldstr:
                        return true;
                }

                return false;
            }
        }

        static bool GetConstantValue(Instruction instruction, out object? value)
        {
            switch (instruction.OpCode.Code)
            {
                case Code.Ldc_I4_0:
                    value = 0;
                    return true;
                case Code.Ldc_I4_1:
                    value = 1;
                    return true;
                case Code.Ldc_I4_2:
                    value = 2;
                    return true;
                case Code.Ldc_I4_3:
                    value = 3;
                    return true;
                case Code.Ldc_I4_4:
                    value = 4;
                    return true;
                case Code.Ldc_I4_5:
                    value = 5;
                    return true;
                case Code.Ldc_I4_6:
                    value = 6;
                    return true;
                case Code.Ldc_I4_7:
                    value = 7;
                    return true;
                case Code.Ldc_I4_8:
                    value = 8;
                    return true;
                case Code.Ldc_I4_M1:
                    value = -1;
                    return true;
                case Code.Ldc_I4:
                    value = (int)instruction.Operand;
                    return true;
                case Code.Ldc_I4_S:
                    value = (int)(sbyte)instruction.Operand;
                    return true;
                case Code.Ldc_I8:
                    value = (long)instruction.Operand;
                    return true;
                case Code.Ldstr:
                    value = (string)instruction.Operand;
                    return true;
                case Code.Ldnull:
                    value = null;
                    return true;
                default:
                    value = null;
                    return false;
            }
        }

        static MethodDefinition? FindSizeMethod(TypeDefinition? type)
        {
            if (type == null)
                return null;

            return type.Methods.First(l => !l.HasMetadataParameters() && l.IsStatic && l.Name == "get_Size");
        }

        readonly struct CallInliner
        {
            readonly MethodBody body;
            readonly UnreachableBlocksOptimizer optimizer;

            public CallInliner(MethodBody body, UnreachableBlocksOptimizer optimizer)
            {
                this.body = body;
                this.optimizer = optimizer;
            }

            public bool RewriteBody()
            {
                bool changed = false;
                LinkerILProcessor processor = body.GetLinkerILProcessor();
#pragma warning disable RS0030 // This optimizer is the reason for the banned API, so it needs to use the Cecil directly
                Collection<Instruction> instrs = body.Instructions;
#pragma warning restore RS0030

                for (int i = 0; i < instrs.Count; ++i)
                {
                    Instruction instr = instrs[i];
                    switch (instr.OpCode.Code)
                    {

                        case Code.Call:
                        case Code.Callvirt:
                            MethodDefinition? md = optimizer._context.TryResolve((MethodReference)instr.Operand);
                            if (md == null)
                                continue;

                            if (md.IsVirtual)
                                continue;

                            if (md.CallingConvention == MethodCallingConvention.VarArg)
                                break;

                            if (md.NoInlining)
                                break;

                            var cpl = new CalleePayload(md, GetArgumentsOnStack(md, instrs, i));
                            MethodResult? call_result = optimizer.TryGetMethodCallResult(cpl);
                            if (call_result is not MethodResult result)
                                break;

                            if (!result.IsSideEffectFree)
                            {
                                optimizer._context.LogMessage($"Cannot inline constant result of '{md.GetDisplayName()}' call due to presence of side effects");
                                break;
                            }

                            if (!md.IsStatic)
                            {
                                if (!md.HasMetadataParameters() && CanInlineInstanceCall(instrs, i))
                                {
                                    processor.Replace(i - 1, Instruction.Create(OpCodes.Nop));
                                    processor.Replace(i, result.GetPrototype());
                                    changed = true;
                                }

                                continue;
                            }

                            if (md.HasMetadataParameters())
                            {
                                if (!IsCalledWithoutSideEffects(md, instrs, i))
                                    continue;

                                for (int p = 1; p <= md.GetMetadataParametersCount(); ++p)
                                {
                                    processor.Replace(i - p, Instruction.Create(OpCodes.Nop));
                                }
                            }

                            processor.Replace(i, result.GetPrototype());
                            changed = true;
                            continue;

                        case Code.Sizeof:
                            var operand = (TypeReference)instr.Operand;
                            Instruction? value = optimizer.GetSizeOfResult(operand);
                            if (value != null)
                            {
                                processor.Replace(i, value.GetPrototype());
                                changed = true;
                            }

                            continue;
                    }
                }

                return changed;
            }

            bool CanInlineInstanceCall(Collection<Instruction> instructions, int index)
            {
                //
                // Instance methods called on `this` have no side-effects
                //
                if (instructions[index - 1].OpCode.Code == Code.Ldarg_0)
                    return !body.Method.IsStatic;

                // More cases can be added later
                return false;
            }

            static bool IsCalledWithoutSideEffects(MethodDefinition method, Collection<Instruction> instructions, int index)
            {
                for (int i = 1; i <= method.GetMetadataParametersCount(); ++i)
                {
                    if (!IsSideEffectFreeLoad(instructions[index - i]))
                        return false;
                }

                return true;
            }
        }

        struct BodyReducer
        {
            readonly LinkContext context;
            Dictionary<Instruction, int>? mapping;

            //
            // Sorted list of body instruction indexes which were
            // replaced pass-through nop
            //
            List<int>? conditionInstrsToRemove;

            //
            // Sorted list of body instruction indexes which were
            // set to be replaced with different intstruction
            //
            List<(int, Instruction)>? conditionInstrsToReplace;

            public BodyReducer(MethodBody body, LinkContext context)
            {
                Body = body;
                this.context = context;

                FoldedInstructions = null;
                mapping = null;
                conditionInstrsToRemove = null;
                conditionInstrsToReplace = null;
                InstructionsReplaced = 0;
            }

            public MethodBody Body { get; }

#pragma warning disable RS0030 // This optimizer is the reason for the banned API, so it needs to use the Cecil directly
            Collection<Instruction> Instructions => Body.Instructions;
            Collection<ExceptionHandler> ExceptionHandlers => Body.ExceptionHandlers;
#pragma warning restore RS0030

            public int InstructionsReplaced { get; set; }

            Collection<Instruction>? FoldedInstructions { get; set; }

            [MemberNotNull(nameof(FoldedInstructions))]
            [MemberNotNull(nameof(mapping))]
            void InitializeFoldedInstruction()
            {
                FoldedInstructions = new Collection<Instruction>(Instructions);
                mapping = new Dictionary<Instruction, int>();
            }

            public void Rewrite(int index, Instruction newInstruction)
            {
                if (FoldedInstructions == null)
                    InitializeFoldedInstruction();

                Debug.Assert(mapping != null);

                // Tracks mapping for replaced instructions for easier
                // branch targets resolution later
                mapping[Instructions[index]] = index;

                FoldedInstructions[index] = newInstruction;
            }

            void RewriteCondition(int index, Instruction instr, int operand)
            {
                switch (instr.OpCode.Code)
                {
                    case Code.Brfalse:
                    case Code.Brfalse_S:
                        if (operand == 0)
                        {
                            Rewrite(index, Instruction.Create(OpCodes.Br, (Instruction)instr.Operand));
                        }
                        else
                        {
                            RewriteConditionToNop(index);
                        }

                        break;
                    case Code.Brtrue:
                    case Code.Brtrue_S:
                        if (operand != 0)
                        {
                            Rewrite(index, Instruction.Create(OpCodes.Br, (Instruction)instr.Operand));
                        }
                        else
                        {
                            RewriteConditionToNop(index);
                        }

                        break;

                    case Code.Switch:
                        var targets = (Instruction[])instr.Operand;
                        if (operand < targets.Length)
                        {
                            // It does not need to be conditional but existing logic in BodySweeper would
                            // need to be updated to deal with 1->2 instruction replacement
                            RewriteConditionTo(index, Instruction.Create(operand == 0 ? OpCodes.Brfalse : OpCodes.Brtrue, targets[operand]));
                            Rewrite(index, Instruction.Create(OpCodes.Br, targets[operand]));
                        }
                        else
                        {
                            RewriteConditionToNop(index);
                        }

                        break;
                }
            }

            void RewriteConditionToNop(int index)
            {
                conditionInstrsToRemove ??= new List<int>();

                conditionInstrsToRemove.Add(index);
                RewriteToNop(index);
            }

            void RewriteConditionTo(int index, Instruction instruction)
            {
                conditionInstrsToReplace ??= new List<(int, Instruction)>();

                conditionInstrsToReplace.Add((index, instruction));
            }

            public void RewriteToNop(int index, int stackDepth)
            {
                if (FoldedInstructions == null)
                    InitializeFoldedInstruction();

                int start_index;
                for (start_index = index; start_index >= 0 && stackDepth > 0; --start_index)
                {
                    stackDepth -= GetStackBehaviourDelta(FoldedInstructions[start_index], out bool undefined);
                    if (undefined)
                        return;
                }

                if (stackDepth != 0)
                {
                    Debug.Fail("Invalid IL?");
                    return;
                }

                while (start_index != index)
                    RewriteToNop(++start_index);
            }

            static int GetStackBehaviourDelta(Instruction instruction, out bool unknown)
            {
                int delta = 0;
                unknown = false;
                switch (instruction.OpCode.StackBehaviourPop)
                {
                    case StackBehaviour.Pop0:
                        break;
                    case StackBehaviour.Pop1:
                    case StackBehaviour.Popref:
                    case StackBehaviour.Popi:
                        --delta;
                        break;
                    case StackBehaviour.Pop1_pop1:
                    case StackBehaviour.Popi_pop1:
                    case StackBehaviour.Popi_popi:
                    case StackBehaviour.Popi_popi8:
                    case StackBehaviour.Popi_popr4:
                    case StackBehaviour.Popi_popr8:
                    case StackBehaviour.Popref_pop1:
                    case StackBehaviour.Popref_popi:
                        delta -= 2;
                        break;
                    case StackBehaviour.Popi_popi_popi:
                    case StackBehaviour.Popref_popi_popi:
                    case StackBehaviour.Popref_popi_popi8:
                    case StackBehaviour.Popref_popi_popr4:
                    case StackBehaviour.Popref_popi_popr8:
                    case StackBehaviour.Popref_popi_popref:
                        delta -= 3;
                        break;
                    case StackBehaviour.Varpop:
                        if (instruction.Operand is IMethodSignature ms)
                        {
                            if (ms.HasThis && instruction.OpCode != OpCodes.Newobj)
                                --delta;

                            delta -= ms.Parameters.Count;
                            break;
                        }

                        if (instruction.OpCode == OpCodes.Ret)
                        {
                            unknown = true;
                            return 0;
                        }

                        Debug.Fail(instruction.Operand?.ToString());
                        unknown = true;
                        return 0;
                    default:
                        Debug.Fail(instruction.OpCode.StackBehaviourPop.ToString());
                        unknown = true;
                        return 0;
                }

                switch (instruction.OpCode.StackBehaviourPush)
                {
                    case StackBehaviour.Push0:
                        break;
                    case StackBehaviour.Push1:
                    case StackBehaviour.Pushi:
                    case StackBehaviour.Pushi8:
                    case StackBehaviour.Pushr4:
                    case StackBehaviour.Pushr8:
                    case StackBehaviour.Pushref:
                        ++delta;
                        break;
                    case StackBehaviour.Push1_push1:
                        delta += 2;
                        break;
                    case StackBehaviour.Varpush:
                        if (instruction.Operand is IMethodSignature ms)
                        {
                            if (ms.ReturnType.MetadataType != MetadataType.Void)
                                ++delta;

                            break;
                        }

                        Debug.Fail(instruction.Operand?.ToString());
                        unknown = true;
                        return 0;
                    default:
                        Debug.Fail(instruction.OpCode.StackBehaviourPush.ToString());
                        unknown = true;
                        return 0;
                }

                return delta;
            }

            void RewriteToNop(int index)
            {
                Rewrite(index, Instruction.Create(OpCodes.Nop));
            }

            public bool RewriteBody()
            {
                if (FoldedInstructions == null)
                    InitializeFoldedInstruction();

                if (!RemoveConditions())
                    return false;

                BitArray reachableInstrs = GetReachableInstructionsMap(out var unreachableEH);
                if (reachableInstrs == null)
                    return false;

                var bodySweeper = new BodySweeper(Body, reachableInstrs, unreachableEH, context);
                bodySweeper.Initialize();

                bodySweeper.Process(conditionInstrsToRemove, conditionInstrsToReplace, out var nopInstructions);
                InstructionsReplaced = bodySweeper.InstructionsReplaced;
                if (InstructionsReplaced == 0)
                    return false;

                reachableInstrs = GetReachableInstructionsMap(out _);
                if (reachableInstrs != null)
                    RemoveUnreachableInstructions(reachableInstrs);

                if (nopInstructions != null)
                {
                    LinkerILProcessor processor = Body.GetLinkerILProcessor();

                    foreach (var instr in nopInstructions)
                        processor.Remove(instr);
                }

                return true;
            }

            public bool ApplyTemporaryInlining(in UnreachableBlocksOptimizer optimizer)
            {
                bool changed = false;
                var instructions = Instructions;
                Instruction? targetResult;

                for (int i = 0; i < instructions.Count; ++i)
                {
                    var instr = instructions[i];
                    switch (instr.OpCode.Code)
                    {

                        case Code.Call:
                        case Code.Callvirt:
                            var md = context.TryResolve((MethodReference)instr.Operand);
                            if (md == null)
                                break;

                            // Not supported
                            if (md.IsVirtual || md.CallingConvention == MethodCallingConvention.VarArg)
                                break;

                            Instruction[]? args = GetArgumentsOnStack(md, FoldedInstructions ?? instructions, i);
                            targetResult = args?.Length > 0 && md.IsStatic ? EvaluateIntrinsicCall(md, args) : null;

                            targetResult ??= optimizer.TryGetMethodCallResult(new CalleePayload(md, args))?.Instruction;

                            if (targetResult == null)
                                break;

                            //
                            // Do simple arguments stack removal by replacing argument expressions with nops. For cases
                            // that require full stack understanding the logic won't work and will leave more opcodes
                            // on the stack and constant won't be propagated
                            //
                            int depth = args?.Length ?? 0;
                            if (!md.IsStatic)
                                ++depth;

                            if (depth != 0)
                                RewriteToNop(i - 1, depth);

                            Rewrite(i, targetResult);
                            changed = true;
                            break;

                        case Code.Ldsfld:
                            var ftarget = (FieldReference)instr.Operand;
                            var field = context.TryResolve(ftarget);
                            if (field == null)
                                break;

                            if (context.Annotations.TryGetFieldUserValue(field, out object? value))
                            {
                                targetResult = CodeRewriterStep.CreateConstantResultInstruction(context, field.FieldType, value);
                                if (targetResult == null)
                                    break;
                                Rewrite(i, targetResult);
                                changed = true;
                            }
                            break;

                        case Code.Sizeof:
                            var operand = (TypeReference)instr.Operand;
                            targetResult = optimizer.GetSizeOfResult(operand);
                            if (targetResult != null)
                            {
                                Rewrite(i, targetResult);
                                changed = true;
                            }
                            break;
                    }
                }

                return changed;
            }

            static bool IsConditionalBranch(OpCode opCode)
            => opCode.Code is Code.Brfalse or Code.Brfalse_S or Code.Brtrue or Code.Brtrue_S;

            void RemoveUnreachableInstructions(BitArray reachable)
            {
                LinkerILProcessor processor = Body.GetLinkerILProcessor();

                int removed = 0;
                for (int i = 0; i < reachable.Count; ++i)
                {
                    if (reachable[i])
                        continue;

                    int index = i - removed;
                    // If we intend to remove the last instruction we replaced it with "ret" above (not "nop")
                    // but we can't get rid of it completely because it may happen that the last kept instruction
                    // is a conditional branch - in which case to keep the IL valid, there has to be something after
                    // the conditional branch instruction (the else branch). So if that's the case
                    // inject "ldnull; throw;" at the end - this branch should never be reachable and it's always valid
                    // (ret may need to return a value of the right type if the method has a return value which is complicated
                    // to construct out of nothing).
                    if (index == Instructions.Count - 1 && Instructions[index].OpCode == OpCodes.Ret &&
                        index > 0 && IsConditionalBranch(Instructions[index - 1].OpCode))
                    {
                        processor.Replace(index, Instruction.Create(OpCodes.Ldnull));
                        processor.InsertAfter(Instructions[index], Instruction.Create(OpCodes.Throw));
                    }
                    else
                    {
                        processor.RemoveAt(index);
                        ++removed;
                    }
                }
            }

            bool RemoveConditions()
            {
                Debug.Assert(FoldedInstructions != null);
                bool changed = false;
                object? left, right;

                //
                // Finds any branchable instruction and checks if the operand or operands
                // can be evaluated as constant result.
                //
                // The logic does not remove any instructions but replaces them with nops for
                // easier processing later (makes the mapping straigh-forward).
                //
                for (int i = 0; i < FoldedInstructions.Count; ++i)
                {
                    var instr = FoldedInstructions[i];
                    var opcode = instr.OpCode;

                    if (opcode.FlowControl == FlowControl.Cond_Branch)
                    {
                        if (opcode.StackBehaviourPop == StackBehaviour.Pop1_pop1)
                        {
                            if (!GetOperandsConstantValues(i, out left, out right))
                                continue;

                            if (left is int lint && right is int rint)
                            {
                                if (IsJumpTargetRange(i - 1, i))
                                    continue;

                                RewriteToNop(i - 2);
                                RewriteToNop(i - 1);

                                if (IsComparisonAlwaysTrue(opcode, lint, rint))
                                {
                                    Rewrite(i, Instruction.Create(OpCodes.Br, (Instruction)instr.Operand));
                                }
                                else
                                {
                                    RewriteConditionToNop(i);
                                }

                                changed = true;
                                continue;
                            }

                            continue;
                        }

                        if (opcode.StackBehaviourPop == StackBehaviour.Popi)
                        {
                            if (i > 0 && GetConstantValue(FoldedInstructions[i - 1], out var operand))
                            {
                                if (operand is int opint)
                                {
                                    if (IsJumpTargetRange(i, i))
                                        continue;

                                    RewriteToNop(i - 1);
                                    RewriteCondition(i, instr, opint);

                                    changed = true;
                                    continue;
                                }

                                if (operand is null && (opcode.Code == Code.Brfalse || opcode.Code == Code.Brfalse_S))
                                {
                                    if (IsJumpTargetRange(i, i))
                                        continue;

                                    RewriteToNop(i - 1);
                                    Rewrite(i, Instruction.Create(OpCodes.Br, (Instruction)instr.Operand));
                                    changed = true;
                                    continue;
                                }
                            }

                            // Common pattern generated by C# compiler in debug mode
                            if (i >= 3 && GetConstantValue(FoldedInstructions[i - 3], out operand) && operand is int opint2 && IsPairedStlocLdloc(FoldedInstructions[i - 2], FoldedInstructions[i - 1]))
                            {
                                if (IsJumpTargetRange(i - 2, i))
                                    continue;

                                RewriteToNop(i - 3);
                                RewriteToNop(i - 2);
                                RewriteToNop(i - 1);
                                RewriteCondition(i, instr, opint2);

                                changed = true;
                                continue;
                            }

                            // Pattern for non-zero based switch with constant input
                            if (i >= 5 && opcode == OpCodes.Switch && GetConstantValue(FoldedInstructions[i - 5], out operand) && operand is int opint3 && IsPairedStlocLdloc(FoldedInstructions[i - 4], FoldedInstructions[i - 3]))
                            {
                                if (IsJumpTargetRange(i - 4, i))
                                    continue;

                                if (!GetConstantValue(FoldedInstructions[i - 2], out operand) || operand is not int offset)
                                    continue;

                                if (FoldedInstructions[i - 1].OpCode != OpCodes.Sub)
                                    continue;

                                RewriteToNop(i - 5);
                                RewriteToNop(i - 4);
                                RewriteToNop(i - 3);
                                RewriteCondition(i, instr, opint3 - offset);

                                changed = true;
                                continue;
                            }

                            continue;
                        }

                        throw new NotImplementedException();
                    }

                    // Mode special for csc in debug mode
                    switch (instr.OpCode.Code)
                    {
                        case Code.Ceq:
                        case Code.Clt:
                        case Code.Cgt:
                            if (!GetOperandsConstantValues(i, out left, out right))
                                continue;

                            if (left is int lint && right is int rint)
                            {
                                if (IsJumpTargetRange(i - 1, i))
                                    continue;

                                RewriteToNop(i - 2);
                                RewriteToNop(i - 1);

                                if (IsComparisonAlwaysTrue(instr.OpCode, lint, rint))
                                {
                                    Rewrite(i, Instruction.Create(OpCodes.Ldc_I4_1));
                                }
                                else
                                {
                                    Rewrite(i, Instruction.Create(OpCodes.Ldc_I4_0));
                                }

                                changed = true;
                            }

                            break;

                        case Code.Cgt_Un:
                            if (!GetOperandsConstantValues(i, out left, out right))
                                continue;

                            if (IsJumpTargetRange(i - 1, i))
                                continue;

                            if (left == null && right == null)
                            {
                                Rewrite(i, Instruction.Create(OpCodes.Ldc_I4_0));
                            }

                            changed = true;
                            break;
                    }
                }

                return changed;
            }

            BitArray GetReachableInstructionsMap(out List<ExceptionHandler>? unreachableHandlers)
            {
                Debug.Assert(FoldedInstructions != null);
                unreachableHandlers = null;
                var reachable = new BitArray(FoldedInstructions.Count);

                Stack<int>? condBranches = null;
                bool exceptionHandlersChecked = !Body.HasExceptionHandlers;
                Instruction target;
                int i = 0;
                while (true)
                {
                    while (i < FoldedInstructions.Count)
                    {
                        if (reachable[i])
                            break;

                        reachable[i] = true;
                        var instr = FoldedInstructions[i++];

                        switch (instr.OpCode.FlowControl)
                        {
                            case FlowControl.Branch:
                                target = (Instruction)instr.Operand;
                                i = GetInstructionIndex(target);
                                continue;

                            case FlowControl.Cond_Branch:
                                condBranches ??= new Stack<int>();

                                switch (instr.Operand)
                                {
                                    case Instruction starget:
                                        condBranches.Push(GetInstructionIndex(starget));
                                        continue;
                                    case Instruction[] mtargets:
                                        foreach (var t in mtargets)
                                            condBranches.Push(GetInstructionIndex(t));
                                        continue;
                                    default:
                                        throw new NotImplementedException();
                                }

                            case FlowControl.Next:
                            case FlowControl.Call:
                            case FlowControl.Meta:
                                continue;

                            case FlowControl.Return:
                            case FlowControl.Throw:
                                break;

                            default:
                                throw new NotImplementedException();
                        }

                        break;
                    }

                    if (condBranches?.Count > 0)
                    {
                        i = condBranches.Pop();
                        continue;
                    }

                    if (!exceptionHandlersChecked)
                    {
                        exceptionHandlersChecked = true;

                        var instrs = Instructions;
                        foreach (var handler in ExceptionHandlers)
                        {
                            int start = instrs.IndexOf(handler.TryStart);
                            int end = instrs.IndexOf(handler.TryEnd) - 1;

                            if (!HasAnyBitSet(reachable, start, end))
                            {
                                unreachableHandlers ??= new List<ExceptionHandler>();

                                unreachableHandlers.Add(handler);
                                continue;
                            }

                            condBranches ??= new Stack<int>();

                            condBranches.Push(GetInstructionIndex(handler.HandlerStart));
                            if (handler.FilterStart != null)
                            {
                                condBranches.Push(GetInstructionIndex(handler.FilterStart));
                                int filterEnd = GetInstructionIndex(handler.HandlerStart) - 1;
                                if (filterEnd >= 0 && FoldedInstructions[filterEnd].OpCode == OpCodes.Endfilter)
                                {
                                    // The endfilter instruction must be at the end of each filter block, even if it's not reachable:
                                    //
                                    // ECMA 335
                                    // I.12.4.2.8.2.5 endfilter:
                                    // 1.Shall appear as the lexically last instruction in the filter.
                                    // [Note: The endfilter is required even if no control - flow path reaches it.This can happen if, for
                                    // example, the filter does a throw.end note]
                                    reachable[filterEnd] = true;
                                }
                            }
                        }

                        if (condBranches?.Count > 0)
                        {
                            i = condBranches.Pop();
                            continue;
                        }
                    }

                    return reachable;
                }
            }

            static bool HasAnyBitSet(BitArray bitArray, int startIndex, int endIndex)
            {
                for (int i = startIndex; i <= endIndex; ++i)
                {
                    if (bitArray[i])
                        return true;
                }

                return false;
            }

            //
            // Returns index of instruction in folded instruction body
            //
            int GetInstructionIndex(Instruction instruction)
            {
                Debug.Assert(FoldedInstructions != null && mapping != null);
                if (mapping.TryGetValue(instruction, out int idx))
                    return idx;

                idx = FoldedInstructions.IndexOf(instruction);
                Debug.Assert(idx >= 0);
                return idx;
            }

            int? TryGetInstructionIndex(Instruction instruction)
            {
                Debug.Assert(mapping != null);
                if (mapping.TryGetValue(instruction, out int idx))
                    return idx;

                return null;
            }

            bool GetOperandsConstantValues(int index, out object? left, out object? right)
            {
                Debug.Assert(FoldedInstructions != null);
                left = default;
                right = default;

                if (index < 2)
                    return false;

                return GetConstantValue(FoldedInstructions[index - 2], out left) &&
                    GetConstantValue(FoldedInstructions[index - 1], out right);
            }

            static bool IsPairedStlocLdloc(Instruction first, Instruction second)
            {
                switch (first.OpCode.Code)
                {
                    case Code.Stloc_0:
                        return second.OpCode.Code == Code.Ldloc_0;
                    case Code.Stloc_1:
                        return second.OpCode.Code == Code.Ldloc_1;
                    case Code.Stloc_2:
                        return second.OpCode.Code == Code.Ldloc_2;
                    case Code.Stloc_3:
                        return second.OpCode.Code == Code.Ldloc_3;
                    case Code.Stloc_S:
                    case Code.Stloc:
                        if (second.OpCode.Code == Code.Ldloc_S || second.OpCode.Code == Code.Ldloc)
                            return ((VariableDefinition)first.Operand).Index == ((VariableDefinition)second.Operand).Index;

                        break;
                }

                return false;
            }

            bool IsJumpTargetRange(int firstInstr, int lastInstr)
            {
                Debug.Assert(FoldedInstructions != null);
                return HasJumpIntoTargetRange(FoldedInstructions, firstInstr, lastInstr, TryGetInstructionIndex);
            }
        }

        struct BodySweeper
        {
            readonly MethodBody body;
#pragma warning disable RS0030 // This optimizer is the reason for the banned API, so it needs to use the Cecil directly
            Collection<Instruction> Instructions => body.Instructions;
            Collection<VariableDefinition> Variables => body.Variables;
            Collection<ExceptionHandler> ExceptionHandlers => body.ExceptionHandlers;
#pragma warning restore RS0030
            readonly BitArray reachable;
            readonly List<ExceptionHandler>? unreachableExceptionHandlers;
            readonly LinkContext context;
            LinkerILProcessor? ilprocessor;
            LinkerILProcessor ILProcessor
            {
                get
                {
                    Debug.Assert(ilprocessor != null);
                    return ilprocessor;
                }
            }

            public BodySweeper(MethodBody body, BitArray reachable, List<ExceptionHandler>? unreachableEH, LinkContext context)
            {
                this.body = body;
                this.reachable = reachable;
                this.unreachableExceptionHandlers = unreachableEH;
                this.context = context;

                InstructionsReplaced = 0;
                ilprocessor = null;
            }

            public int InstructionsReplaced { get; set; }

            public void Initialize()
            {
                var instrs = Instructions;

                //
                // Reusing same reachable map and altering it at indexes
                // which will remain same during replacement processing
                //
                for (int i = 0; i < instrs.Count; ++i)
                {
                    if (reachable[i])
                        continue;

                    var instr = instrs[i];
                    switch (instr.OpCode.Code)
                    {
                        case Code.Nop:
                            reachable[i] = true;
                            continue;

                        case Code.Ret:
                            if (i == instrs.Count - 1)
                                reachable[i] = true;

                            break;
                    }
                }

                ilprocessor = body.GetLinkerILProcessor();
            }

            public void Process(List<int>? conditionInstrsToRemove, List<(int, Instruction)>? conditionInstrsToReplace, out List<Instruction>? sentinelNops)
            {
                List<VariableDefinition>? removedVariablesReferences = null;
                var instrs = Instructions;

                //
                // Process list of conditional instructions that were set to be replaced and not removed
                //
                if (conditionInstrsToReplace != null)
                {
                    foreach (var pair in conditionInstrsToReplace)
                    {
                        var instr = instrs[pair.Item1];
                        switch (instr.OpCode.StackBehaviourPop)
                        {
                            case StackBehaviour.Popi:
                                ILProcessor.Replace(pair.Item1, pair.Item2);
                                InstructionsReplaced++;
                                break;
                            default:
                                Debug.Fail("not supported");
                                break;
                        }
                    }

                }

                //
                // Initial pass which replaces unreachable instructions with nops or
                // ret to keep the body verifiable
                //
                for (int i = 0; i < instrs.Count; ++i)
                {
                    if (reachable[i])
                        continue;

                    var instr = instrs[i];

                    Instruction newInstr;
                    if (i == instrs.Count - 1)
                    {
                        newInstr = Instruction.Create(OpCodes.Ret);
                    }
                    else
                    {
                        newInstr = Instruction.Create(OpCodes.Nop);
                    }

                    ILProcessor.Replace(i, newInstr);
                    InstructionsReplaced++;

                    VariableDefinition? variable = GetVariableReference(instr);
                    if (variable != null)
                    {
                        removedVariablesReferences ??= new List<VariableDefinition>();
                        if (!removedVariablesReferences.Contains(variable))
                            removedVariablesReferences.Add(variable);
                    }
                }

                CleanExceptionHandlers();

                sentinelNops = null;

                //
                // Process list of conditional jump which should be removed. They cannot be
                // replaced with nops as they alter the stack
                //
                if (conditionInstrsToRemove != null)
                {
                    int bodyExpansion = 0;

                    foreach (int instrIndex in conditionInstrsToRemove)
                    {
                        var index = instrIndex + bodyExpansion;
                        var instr = instrs[index];

                        switch (instr.OpCode.StackBehaviourPop)
                        {
                            case StackBehaviour.Pop1_pop1:

                                InstructionsReplaced += 2;

                                //
                                // One of the operands is most likely constant and could just be removed instead of additional pop
                                //
                                if (index > 0 && IsSideEffectFreeLoad(instrs[index - 1]))
                                {
                                    var nop = Instruction.Create(OpCodes.Nop);

                                    sentinelNops ??= new List<Instruction>();
                                    sentinelNops.Add(nop);

                                    ILProcessor.Replace(index - 1, Instruction.Create(OpCodes.Pop));
                                    ILProcessor.Replace(index, nop);
                                }
                                else
                                {
                                    var pop = Instruction.Create(OpCodes.Pop);
                                    ILProcessor.Replace(index, pop);
                                    ILProcessor.InsertAfter(pop, Instruction.Create(OpCodes.Pop));

                                    //
                                    // conditionInstrsToRemove is always sorted and instead of
                                    // increasing remaining indexes we introduce index delta value
                                    //
                                    bodyExpansion++;
                                }
                                break;
                            case StackBehaviour.Popi:
                                ILProcessor.Replace(index, Instruction.Create(OpCodes.Pop));
                                InstructionsReplaced++;
                                break;
                        }
                    }
                }

                //
                // Replacing instructions with nops can make local variables unused. Process them
                // as the last step to reduce more type dependencies
                //
                if (removedVariablesReferences != null)
                {
                    CleanRemovedVariables(removedVariablesReferences);
                }
            }

            void CleanRemovedVariables(List<VariableDefinition> variables)
            {
                foreach (var instr in Instructions)
                {
                    VariableDefinition? variable = GetVariableReference(instr);
                    if (variable == null)
                        continue;

                    if (!variables.Remove(variable))
                        continue;

                    if (variables.Count == 0)
                        return;
                }

                variables.Sort((a, b) => b.Index.CompareTo(a.Index));
                var body_variables = Variables;

                foreach (var variable in variables)
                {
                    var index = body_variables.IndexOf(variable);

                    //
                    // Remove variable only if it's the last one. Instead of
                    // re-indexing all variables change it to System.Object,
                    // which is enough to drop the dependency
                    //
                    if (index == body_variables.Count - 1)
                    {
                        body_variables.RemoveAt(index);
                    }
                    else
                    {
                        var objectType = BCL.FindPredefinedType(WellKnownType.System_Object, context);
                        body_variables[index].VariableType = objectType ?? throw new NotSupportedException("Missing predefined 'System.Object' type");
                    }
                }
            }

            void CleanExceptionHandlers()
            {
                if (unreachableExceptionHandlers == null)
                    return;

                foreach (var eh in unreachableExceptionHandlers)
                    ExceptionHandlers.Remove(eh);
            }

            VariableDefinition? GetVariableReference(Instruction instruction)
            {
                switch (instruction.OpCode.Code)
                {
                    case Code.Stloc_0:
                    case Code.Ldloc_0:
                        return Variables[0];
                    case Code.Stloc_1:
                    case Code.Ldloc_1:
                        return Variables[1];
                    case Code.Stloc_2:
                    case Code.Ldloc_2:
                        return Variables[2];
                    case Code.Stloc_3:
                    case Code.Ldloc_3:
                        return Variables[3];
                }

                if (instruction.Operand is VariableReference vr)
                    return vr.Resolve();

                return null;
            }
        }

        struct ConstantExpressionMethodAnalyzer
        {
            readonly LinkContext context;
            readonly UnreachableBlocksOptimizer optimizer;

            Stack<Instruction>? stack_instr;
            Dictionary<int, Instruction>? locals;

            public ConstantExpressionMethodAnalyzer(UnreachableBlocksOptimizer optimizer)
            {
                this.optimizer = optimizer;
                this.context = optimizer._context;
                stack_instr = null;
                locals = null;
                Result = null;
                SideEffectFreeResult = true;
            }

            //
            // Single expression that is representing the evaluation result with the specific
            // callee arguments
            //
            public Instruction? Result { get; private set; }

            //
            // Returns true when the method evaluation with specific arguments does not cause
            // any observable side effect (e.g. possible NRE, field access, etc)
            //
            public bool SideEffectFreeResult { get; private set; }

            [MemberNotNullWhen(true, nameof(Result))]
            public bool Analyze(in CalleePayload callee, Stack<MethodDefinition> callStack)
            {
                MethodDefinition method = callee.Method;
                Instruction[]? arguments = callee.Arguments;
#pragma warning disable RS0030 // This optimizer is the reason for the banned API, so it needs to use the Cecil directly
                Collection<Instruction> instructions = callee.Method.Body.Instructions;
#pragma warning restore RS0030
                MethodBody body = method.Body;

                VariableReference vr;
                Instruction? jmpTarget = null;
                Instruction? linstr;
                object? left, right, operand;

                SideEffectFreeResult = !HasSideEffects(method);

                //
                // We could implement a full-blown interpreter here but for now, it handles
                // cases used in runtime libraries
                //
                for (int i = 0; i < instructions.Count; ++i)
                {
                    var instr = instructions[i];

                    if (jmpTarget != null)
                    {
                        //
                        // Handles both backward and forward jumps
                        //
                        if (instr != jmpTarget)
                            continue;

                        jmpTarget = null;
                    }

                    switch (instr.OpCode.Code)
                    {
                        case Code.Nop:
                        case Code.Volatile:
                            continue;
                        case Code.Pop:
                            Debug.Assert(stack_instr != null, "invalid il?");
                            stack_instr?.Pop();
                            continue;

                        case Code.Br_S:
                        case Code.Br:
                            jmpTarget = (Instruction)instr.Operand;
                            continue;

                        case Code.Brfalse_S:
                        case Code.Brfalse:
                        {
                            if (!GetOperandConstantValue(out operand))
                                return false;

                            if (operand is int oint)
                            {
                                if (oint == 0)
                                    jmpTarget = (Instruction)instr.Operand;

                                continue;
                            }

                            return false;
                        }

                        case Code.Brtrue_S:
                        case Code.Brtrue:
                        {
                            if (!GetOperandConstantValue(out operand))
                                return false;

                            if (operand is int oint)
                            {
                                if (oint != 0)
                                    jmpTarget = (Instruction)instr.Operand;

                                continue;
                            }

                            return false;
                        }

                        case Code.Beq:
                        case Code.Beq_S:
                        case Code.Bne_Un:
                        case Code.Bne_Un_S:
                        case Code.Bge:
                        case Code.Bge_S:
                        case Code.Bge_Un:
                        case Code.Bge_Un_S:
                        case Code.Bgt:
                        case Code.Bgt_S:
                        case Code.Bgt_Un:
                        case Code.Bgt_Un_S:
                        case Code.Ble:
                        case Code.Ble_S:
                        case Code.Ble_Un:
                        case Code.Ble_Un_S:
                        case Code.Blt:
                        case Code.Blt_S:
                        case Code.Blt_Un:
                        case Code.Blt_Un_S:
                            if (EvaluateConditionalJump(instr, out jmpTarget))
                                continue;
                            return false;

                        case Code.Ldc_I4:
                        case Code.Ldc_I4_S:
                        case Code.Ldc_I4_0:
                        case Code.Ldc_I4_1:
                        case Code.Ldc_I4_2:
                        case Code.Ldc_I4_3:
                        case Code.Ldc_I4_4:
                        case Code.Ldc_I4_5:
                        case Code.Ldc_I4_6:
                        case Code.Ldc_I4_7:
                        case Code.Ldc_I4_8:
                        case Code.Ldc_I4_M1:
                        case Code.Ldc_I8:
                        case Code.Ldnull:
                        case Code.Ldstr:
                        case Code.Ldtoken:
                            PushOnStack(instr);
                            continue;

                        case Code.Ldloc_0:
                            linstr = GetLocalsValue(0, body);
                            if (linstr == null)
                                return false;

                            PushOnStack(linstr);
                            continue;
                        case Code.Ldloc_1:
                            linstr = GetLocalsValue(1, body);
                            if (linstr == null)
                                return false;

                            PushOnStack(linstr);
                            continue;
                        case Code.Ldloc_2:
                            linstr = GetLocalsValue(2, body);
                            if (linstr == null)
                                return false;

                            PushOnStack(linstr);
                            continue;
                        case Code.Ldloc_3:
                            linstr = GetLocalsValue(3, body);
                            if (linstr == null)
                                return false;

                            PushOnStack(linstr);
                            continue;
                        case Code.Ldloc:
                        case Code.Ldloc_S:
                            vr = (VariableReference)instr.Operand;
                            linstr = GetLocalsValue(vr.Index, body);
                            if (linstr == null)
                                return false;

                            PushOnStack(linstr);
                            continue;
                        case Code.Stloc_0:
                            StoreToLocals(0);
                            continue;
                        case Code.Stloc_1:
                            StoreToLocals(1);
                            continue;
                        case Code.Stloc_2:
                            StoreToLocals(2);
                            continue;
                        case Code.Stloc_3:
                            StoreToLocals(3);
                            continue;
                        case Code.Stloc_S:
                        case Code.Stloc:
                            vr = (VariableReference)instr.Operand;
                            StoreToLocals(vr.Index);
                            continue;

                        case Code.Ldarg_0:
                            if (!method.IsStatic)
                            {
                                PushOnStack(instr);
                                continue;
                            }

                            linstr = GetArgumentValue(arguments, 0);
                            if (linstr == null)
                                return false;

                            PushOnStack(linstr);
                            continue;

                        case Code.Ldarg_1:
                            if (!method.IsStatic)
                                return false;

                            linstr = GetArgumentValue(arguments, 1);
                            if (linstr == null)
                                return false;

                            PushOnStack(linstr);
                            continue;

                        case Code.Ldsfld:
                        {
                            var ftarget = (FieldReference)instr.Operand;
                            FieldDefinition? field = context.TryResolve(ftarget);
                            if (field == null)
                                return false;

                            if (context.Annotations.TryGetFieldUserValue(field, out object? value))
                            {
                                linstr = CodeRewriterStep.CreateConstantResultInstruction(context, field.FieldType, value);
                                if (linstr == null)
                                    return false;
                            }
                            else
                            {
                                SideEffectFreeResult = false;
                                linstr = instr;
                            }

                            PushOnStack(linstr);
                            continue;
                        }

                        case Code.Ceq:
                        {
                            if (!GetOperandsConstantValues(out right, out left))
                                return false;

                            if (left is int lint && right is int rint)
                            {
                                PushOnStack(Instruction.Create(OpCodes.Ldc_I4, lint == rint ? 1 : 0));
                                continue;
                            }

                            if (left is long llong && right is long rlong)
                            {
                                PushOnStack(Instruction.Create(OpCodes.Ldc_I4, llong == rlong ? 1 : 0));
                                continue;
                            }

                            return false;
                        }

                        case Code.Conv_I8:
                        {
                            if (!GetOperandConstantValue(out operand))
                                return false;

                            if (operand is int oint)
                            {
                                PushOnStack(Instruction.Create(OpCodes.Ldc_I8, (long)oint));
                                continue;
                            }

                            // TODO: Handle more types
                            return false;
                        }

                        case Code.Call:
                        case Code.Callvirt:
                        {
                            MethodReference mr = (MethodReference)instr.Operand;
                            MethodDefinition? md = optimizer._context.TryResolve(mr);
                            if (md == null || md == method)
                                return false;

                            if (md.IsVirtual)
                                return false;

                            Instruction[]? args;
                            if (!md.HasMetadataParameters())
                            {
                                args = Array.Empty<Instruction>();
                            }
                            else
                            {
                                //
                                // Don't need to check for ref/out because ldloca like instructions are not supported
                                //
                                args = GetArgumentsOnStack(md);
                                if (args == null)
                                    return false;
                            }

                            if (md.ReturnType.MetadataType == MetadataType.Void)
                            {
                                // For now consider all void methods as side-effect causing
                                SideEffectFreeResult = false;
                                continue;
                            }

                            if (!md.IsStatic && !CanEvaluateInstanceMethodCall(method))
                                return false;

                            //
                            // Evaluate known framework methods
                            //
                            if (args.Length > 0)
                            {
                                linstr = EvaluateIntrinsicCall(md, args);
                                if (linstr != null)
                                {
                                    PushOnStack(linstr);
                                    continue;
                                }
                            }

                            //
                            // Guard against stack overflow on recursive calls. This could be turned into
                            // a warning if we check arguments too
                            //
                            if (callStack.Contains(md))
                                return false;

                            callStack.Push(method);
                            MethodResult? call_result = optimizer.TryGetMethodCallResult(new CalleePayload(md, args), callStack);
                            if (!callStack.TryPop(out _))
                                return false;

                            if (call_result is MethodResult result)
                            {
                                if (!result.IsSideEffectFree)
                                    SideEffectFreeResult = false;

                                PushOnStack(result.Instruction);
                                continue;
                            }

                            return false;
                        }

                        case Code.Sizeof:
                        {
                            var type = (TypeReference)instr.Operand;
                            linstr = optimizer.GetSizeOfResult(type);
                            if (linstr != null)
                            {
                                PushOnStack(linstr);
                                continue;
                            }

                            return false;
                        }

                        case Code.Ret:
                            if (ConvertStackToResult())
                                return true;

                            break;
                    }

                    return false;
                }

                return false;
            }

            bool CanEvaluateInstanceMethodCall(MethodDefinition context)
            {
                if (stack_instr == null || !stack_instr.TryPop(out Instruction? instr))
                    return false;

                switch (instr.OpCode.Code)
                {
                    case Code.Ldarg_0:
                        if (!context.IsStatic)
                            return true;

                        goto default;
                    default:
                        // We are not inlining hence can evaluate anything and decide later
                        // how to handle sitation when the result is not deterministic
                        SideEffectFreeResult = false;
                        return true;
                }
            }

            bool EvaluateConditionalJump(Instruction instr, out Instruction? target)
            {
                if (!GetOperandsConstantValues(out object? right, out object? left))
                {
                    target = null;
                    return false;
                }

                if (left is int lint && right is int rint)
                {
                    if (IsComparisonAlwaysTrue(instr.OpCode, lint, rint))
                        target = (Instruction)instr.Operand;
                    else
                        target = null;

                    return true;
                }

                target = null;
                return false;
            }

            [MemberNotNullWhen(true, nameof(Result))]
            bool ConvertStackToResult()
            {
                if (stack_instr == null)
                    return false;

                if (stack_instr.Count != 1)
                    return false;

                var instr = stack_instr.Pop();

                switch (instr.OpCode.Code)
                {
                    case Code.Ldc_I4_0:
                    case Code.Ldc_I4_1:
                    case Code.Ldc_I4_2:
                    case Code.Ldc_I4_3:
                    case Code.Ldc_I4_4:
                    case Code.Ldc_I4_5:
                    case Code.Ldc_I4_6:
                    case Code.Ldc_I4_7:
                    case Code.Ldc_I4_8:
                    case Code.Ldc_I4:
                    case Code.Ldc_I4_S:
                    case Code.Ldc_I4_M1:
                    case Code.Ldc_I8:
                    case Code.Ldnull:
                    case Code.Ldstr:
                        Result = instr;
                        return true;
                }

                return false;
            }

            static Instruction? GetArgumentValue(Instruction[]? arguments, int index)
            {
                if (arguments == null)
                    return null;

                return index < arguments.Length ? arguments[index] : null;
            }

            Instruction[]? GetArgumentsOnStack(MethodDefinition method)
            {
                int length = method.GetMetadataParametersCount();
                Debug.Assert(length != 0);
                if (stack_instr?.Count < length)
                    return null;

                var result = new Instruction[length];
                while (length != 0)
                    result[--length] = stack_instr!.Pop();

                return result;
            }

            Instruction? GetLocalsValue(int index, MethodBody body)
            {
                if (locals != null && locals.TryGetValue(index, out Instruction? instruction))
                    return instruction;

                if (!body.InitLocals)
                    return null;

#pragma warning disable RS0030 // This optimizer is the reason for the banned API, so it needs to use the Cecil directly
                var variables = body.Variables;
#pragma warning restore RS0030

                // local variables don't need to be explicitly initialized
                return CodeRewriterStep.CreateConstantResultInstruction(context, variables[index].VariableType);
            }

            bool GetOperandConstantValue([NotNullWhen(true)] out object? value)
            {
                if (stack_instr == null)
                {
                    value = null;
                    return false;
                }

                Instruction? instr;
                if (!stack_instr.TryPop(out instr))
                {
                    value = null;
                    return false;
                }

                return GetConstantValue(instr, out value);
            }

            bool GetOperandsConstantValues([NotNullWhen(true)] out object? left, [NotNullWhen(true)] out object? right)
            {
                if (stack_instr == null)
                {
                    left = right = null;
                    return false;
                }

                Instruction? instr;
                if (!stack_instr.TryPop(out instr))
                {
                    left = right = null;
                    return false;
                }

                if (instr == null)
                {
                    left = right = null;
                    return false;
                }

                if (!GetConstantValue(instr, out left))
                {
                    left = right = null;
                    return false;
                }

                if (!stack_instr.TryPop(out instr))
                {
                    left = right = null;
                    return false;
                }

                if (instr is null)
                {
                    left = right = null;
                    return false;
                }

                return GetConstantValue(instr, out right);
            }

            void PushOnStack(Instruction instruction)
            {
                stack_instr ??= new Stack<Instruction>();

                stack_instr.Push(instruction);
            }

            void StoreToLocals(int index)
            {
                locals ??= new Dictionary<int, Instruction>();

                if (stack_instr == null)
                    Debug.Fail("Invalid IL?");
                locals[index] = stack_instr.Pop();
            }
        }

        readonly record struct CalleePayload(MethodDefinition Method, Instruction[]? Arguments = null)
        {
            public bool HasUnknownArguments => Arguments is null;
        }

        readonly record struct MethodResult(Instruction Instruction, bool IsSideEffectFree)
        {
            public Instruction GetPrototype() => Instruction.GetPrototype();
        }
    }
}
